﻿#region

using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using Rnwood.SmtpServer.Extensions;
using Rnwood.SmtpServer.Verbs;
using System.Threading;

#endregion

namespace Rnwood.SmtpServer
{
    public class Connection : IConnection
    {
        private readonly Encoding _currentReaderEncoding;
        private readonly Encoding _sevenBitASCIIEncoding;
        private readonly TcpClient _tcpClient;
        private StreamReader _reader;

        private Stream _stream;
        private StreamWriter _writer;

        /// <summary>
        /// Creates a new connection object. Sets up the processor verbs to process the SMTP 
        /// commands issued by the client. Gets the underlying streams (both reader and writer)
        /// for the TCP Connection
        /// </summary>
        /// <param name="server">The server object with which the client is initiating the connection</param>
        /// <param name="tcpClient">Client who is initiating the connection</param>
        public Connection(Server server, TcpClient tcpClient)
        {
            VerbMap = new VerbMap();
            Session = new Session()
                          {
                              ClientAddress = ((IPEndPoint) tcpClient.Client.RemoteEndPoint).Address,
                              StartDate = DateTime.Now
                          };

            Server = server;
            _tcpClient = tcpClient;
            _tcpClient.ReceiveTimeout = Server.Behaviour.GetReceiveTimeout(this);

            _currentReaderEncoding =
                _sevenBitASCIIEncoding =
                Encoding.GetEncoding("ASCII", new EncoderExceptionFallback(), new ASCIITruncatingDecoderFallback());
            _stream = tcpClient.GetStream();

            SetupReaderAndWriter();
            SetupVerbs();
        }

        #region IConnectionProcessor Members

        public Server Server { get; private set; }

        public void SwitchReaderEncoding(Encoding encoding)
        {
            SetupReaderAndWriter();
        }

        public void SwitchReaderEncodingToDefault()
        {
            SwitchReaderEncoding(_sevenBitASCIIEncoding);
        }

        public IExtensionProcessor[] ExtensionProcessors { get; private set; }
        
        /// <summary>
        /// Properly close the underlying streams and connection object and dispose off 
        /// the Stream object (necessary, will cause memory leak under heavy loads otherwise
        /// </summary>
        public void CloseConnection()
        {
            _writer.Flush();
            _tcpClient.Close();
            _writer.Dispose();
        }

        public VerbMap VerbMap { get; private set; }

        public void ApplyStreamFilter(Func<Stream, Stream> filter)
        {
            _stream = filter(_stream);
            SetupReaderAndWriter();
        }

        public MailVerb MailVerb
        {
            get { return (MailVerb) VerbMap.GetVerbProcessor("MAIL"); }
        }

        public void WriteLine(string text, params object[] arg)
        {
            string formattedText = string.Format(text, arg);
            Session.AppendToLog(formattedText);
            _writer.WriteLine(formattedText);
        }

        public void WriteResponse(SmtpResponse response)
        {
            WriteLine(response.ToString().TrimEnd());
        }

        public string ReadLine()
        {
            string text = _reader.ReadLine();
            Session.AppendToLog(text);
            return text;

        }

        public Session Session { get; private set; }

        public Message CurrentMessage { get; private set; }

        public Message NewMessage()
        {
            CurrentMessage = new Message(Session);
            return CurrentMessage;
        }

        /// <summary>
        /// When the message transmission is complete, form a Message object from 
        /// the raw data and enqueue it in the incoming queue
        /// </summary>
        public void CommitMessage()
        {
            Message message = CurrentMessage;
            Session.Messages.Add(message);

            // Add in Concurrent Queue 
            Server.MessageQueue.Enqueue(message);
            CurrentMessage = null;

            // Call the corresponding handler so that it can wake up sleeping threads
            // in case they were idle.
            Server.Behaviour.OnMessageReceived(this, message);
        }

        /// <summary>
        /// Processes the message before passing it from the incoming queue to the active
        /// queue
        /// </summary>
        /// <param name="m">The message to be processed</param>
        private void ProcessMessage(Object m)
        {
            Message msg = (Message)m;
            Server.msgProcessor.Process(msg);
        }

        public void AbortMessage()
        {
            CurrentMessage = null;
       
        }

        #endregion

        /// <summary>
        /// Sets up the Reader and Writer Streams for the Connection
        /// </summary>
        private void SetupReaderAndWriter()
        {
            _writer = new StreamWriter(_stream, _currentReaderEncoding) {AutoFlush = true, NewLine = "\r\n"};
            _reader = new StreamReader(_stream, _currentReaderEncoding);
        }

        private void SetupVerbs()
        {
            VerbMap.SetVerbProcessor("HELO", new HeloVerb());
            VerbMap.SetVerbProcessor("EHLO", new EhloVerb());
            VerbMap.SetVerbProcessor("QUIT", new QuitVerb());
            VerbMap.SetVerbProcessor("MAIL", new MailVerb());
            VerbMap.SetVerbProcessor("RCPT", new RcptVerb());
            VerbMap.SetVerbProcessor("DATA", new DataVerb());
            VerbMap.SetVerbProcessor("RSET", new RsetVerb());
            VerbMap.SetVerbProcessor("NOOP", new NoopVerb());

            ExtensionProcessors =
                Server.Behaviour.GetExtensions(this).Select(e => e.CreateExtensionProcessor(this)).ToArray();
        }

        /// <summary>
        /// Start the connection with the client.
        /// </summary>
        public void Start()
        {
            try
            {
                // Use the Server Behaviour object defined earlier to get the server characteristics
                Server.Behaviour.OnSessionStarted(this, Session);

                // If SSL is enabled for the server, create the underlying secure stream using the SSL
                // certificate
                if (Server.Behaviour.IsSSLEnabled(this))
                {
                    SslStream sslStream = new SslStream(_stream);
                    sslStream.AuthenticateAsServer(Server.Behaviour.GetSSLCertificate(this));
                    _stream = sslStream;
                    Session.SecureConnection = true;
                }

                // Connection establishment complete, let the client know we are ready to receive the data
                WriteResponse(new SmtpResponse(StandardSmtpResponseCode.ServiceReady,
                                               Server.Behaviour.DomainName + " smtp4dev ready"));

                // Do as long as we are connected to client, a client may send multiple messages on a single 
                // connection
                while (_tcpClient.Client.Connected)
                {
                    // Read the raw text and find out and process the corresponding command by calling 
                    // up the appropriate command processor
                    SmtpCommand command = new SmtpCommand(ReadLine());
                    Server.Behaviour.OnCommandReceived(this, command);

                    if (command.IsValid)
                    {
                        // Get the appropriate Verb Processor if syntax is valid
                        IVerb verbProcessor = VerbMap.GetVerbProcessor(command.Verb);

                        if (verbProcessor != null)
                        {
                            try
                            {
                                // Process the command
                                verbProcessor.Process(this, command);
                            }
                            catch (SmtpServerException exception)
                            {
                                WriteResponse(exception.SmtpResponse);
                            }
                        }
                        else
                        {
                            WriteResponse(new SmtpResponse(StandardSmtpResponseCode.SyntaxErrorCommandUnrecognised,
                                                           "Command unrecognised"));
                        }
                    }
                    else if (command.IsEmpty)
                    {
                    }
                    else
                    {
                        WriteResponse(new SmtpResponse(StandardSmtpResponseCode.SyntaxErrorCommandUnrecognised,
                                                       "Command unrecognised"));
                    }
                }
            }
            // Problem with I/O? Log the exception into the session object
            catch (IOException ioException)
            {
                Session.SessionError = ioException.Message;
            }

            // Call the CloseConnection() method to make sure resources are freed
            CloseConnection();

            Session.EndDate = DateTime.Now;
            Server.Behaviour.OnSessionCompleted(this, Session);
        }
    }
}